Skip to content

fix: force process exit when a worker thread can not be terminated - #9793

Closed
nflaig wants to merge 3 commits into
unstablefrom
nflaig/fast-exit-stuck-worker
Closed

fix: force process exit when a worker thread can not be terminated#9793
nflaig wants to merge 3 commits into
unstablefrom
nflaig/fast-exit-stuck-worker

Conversation

@nflaig

@nflaig nflaig commented Aug 9, 2026

Copy link
Copy Markdown
Member

Follow up to #9790. That one keeps a stuck network worker from costing us the state archive, this one keeps it from costing us a 60s shutdown.

process.exit() joins every worker thread via stop_sub_worker_contexts() -> uv_thread_join(), so a worker that can not be terminated blocks the exit until the process manager gives up. Nothing in JS can preempt that, the main thread is inside C++ and the event loop is dead, so no timer runs. Captured live from a wedged process:

Thread 1 "MainThread":
#2  uv_thread_join                    deps/uv/src/unix/thread.c:295
#3  node::worker::Worker::JoinThread()
#4  node::Environment::stop_sub_worker_contexts()
#5  node::DefaultProcessExitHandlerInternal(...)

A signal is the only lever. terminateWorkerThread now records the failure process wide, and once BeaconNode.close() has returned, the CLI leaves on its own terms. That point is safe, the finalized state is archived and the db is closed.

This only works if the process is not pid 1

The kernel discards signals sent to pid 1 that have no handler installed, including SIGKILL. A container running the node directly as pid 1 therefore can not signal itself at all. Verified:

result
docker run node -e 'process.kill(process.pid,"SIGKILL")' pid=1, survives, exit 0
same with docker run --init pid=7, killed immediately

Measured end to end on a mainnet node, with an init so the node is not pid 1, and the failed-termination path forced:

stopSeconds=6.7  exit=143  dockerd force-kill: 0
warn: Worker thread still running, forcing process exit

and without an init the same build survives its own SIGTERM and SIGKILL.

So when the signal is not deliverable this logs an actionable error naming the requirement and falls through to the blocking process.exit(), which is no worse than today's behaviour. Deployments that want the fast exit need docker --init, init: true in compose, tini, or systemd (where the node is never pid 1).

Worth considering as a follow up: warn at startup when process.pid === 1, so operators learn about it before it matters rather than during a shutdown.

Testing

3 clean shutdowns on the final build: exit 0 in 6.0-7.7s, force-exit branch correctly not taken. Forced-failure path: exit 143 in 6.7s with zero force kills. The stuck worker itself is intermittent (~10% of shutdowns) and unrelated to this change, root cause notes at https://gist.github.com/nflaig/5f41cfc50f38baf5046a034162943dc3

AI Assistance Disclosure

Investigation and patch developed with Claude Code, validated on a mainnet node as above.

馃 Generated with Claude Code

`process.exit()` joins every worker via `stop_sub_worker_contexts()`, so a worker
that can not be terminated blocks the exit until the process manager gives up,
60s under docker and longer elsewhere. Nothing in JS can preempt that, the main
thread is inside `uv_thread_join` so no timer runs.

Record the failed termination process wide, it is a property of the process
rather than of any one core, and leave on our own terms once `BeaconNode.close()`
has returned. Safe at that point, the state is archived and the db is closed.

Note this only works if the process is not pid 1. The kernel discards signals
sent to pid 1 that have no handler installed, including SIGKILL, so a container
running the node as pid 1 needs an init (docker --init, tini) for the exit to be
deliverable. It logs an error and falls through to the blocking `process.exit()`
otherwise, which is no worse than the current behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃挕 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 624c336201

鈩癸笍 About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 馃憤.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

logger?.debug(`Worker thread failed to terminate in ${retryCount * retryMs}ms`);
someWorkerTerminationFailed = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the failure state after late worker termination

If the worker misses the final retry deadline but terminates while the remaining BeaconNode.close() steps are running, this flag remains permanently true. The handler then sends itself SIGTERM even though no worker remains to block process.exit(), changing a successful shutdown from exit 0 to exit 143. Track workers that are currently unterminated, or clear the state when the existing termination subscription receives a late event.

Useful? React with 馃憤聽/ 馃憥.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Performance Report

鉁旓笍 no performance regression detected

Full benchmark results
Benchmark suite Current: 1ebe9c4 Previous: de5f89e Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 832.74 us/op 836.62 us/op 1.00
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 38.158 us/op 39.187 us/op 0.97
BLS verify - blst 734.94 us/op 641.83 us/op 1.15
BLS verifyMultipleSignatures 3 - blst 1.3785 ms/op 1.3627 ms/op 1.01
BLS verifyMultipleSignatures 8 - blst 2.1918 ms/op 2.1629 ms/op 1.01
BLS verifyMultipleSignatures 32 - blst 6.8419 ms/op 6.9541 ms/op 0.98
BLS verifyMultipleSignatures 64 - blst 13.896 ms/op 13.398 ms/op 1.04
BLS verifyMultipleSignatures 128 - blst 25.929 ms/op 25.436 ms/op 1.02
BLS deserializing 10000 signatures 620.29 ms/op 629.83 ms/op 0.98
BLS deserializing 100000 signatures 6.3568 s/op 6.3196 s/op 1.01
BLS verifyMultipleSignatures - same message - 3 - blst 811.09 us/op 799.72 us/op 1.01
BLS verifyMultipleSignatures - same message - 8 - blst 894.78 us/op 939.08 us/op 0.95
BLS verifyMultipleSignatures - same message - 32 - blst 1.4642 ms/op 1.5581 ms/op 0.94
BLS verifyMultipleSignatures - same message - 64 - blst 2.3250 ms/op 2.3823 ms/op 0.98
BLS verifyMultipleSignatures - same message - 128 - blst 4.0416 ms/op 4.0610 ms/op 1.00
BLS aggregatePubkeys 32 - blst 17.721 us/op 17.543 us/op 1.01
BLS aggregatePubkeys 128 - blst 63.191 us/op 62.665 us/op 1.01
getSlashingsAndExits - default max 49.351 us/op 50.951 us/op 0.97
getSlashingsAndExits - 2k 341.44 us/op 402.73 us/op 0.85
proposeBlockBody type=full, size=empty 672.87 us/op 660.73 us/op 1.02
isKnown best case - 1 super set check 192.00 ns/op 161.00 ns/op 1.19
isKnown normal case - 2 super set checks 167.00 ns/op 156.00 ns/op 1.07
isKnown worse case - 16 super set checks 168.00 ns/op 157.00 ns/op 1.07
validate api signedAggregateAndProof - struct 1.5553 ms/op 1.5177 ms/op 1.02
validate gossip signedAggregateAndProof - struct 1.5456 ms/op 1.5120 ms/op 1.02
batch validate gossip attestation - vc 640000 - chunk 32 110.11 us/op 108.01 us/op 1.02
batch validate gossip attestation - vc 640000 - chunk 64 95.461 us/op 93.534 us/op 1.02
batch validate gossip attestation - vc 640000 - chunk 128 87.556 us/op 86.773 us/op 1.01
batch validate gossip attestation - vc 640000 - chunk 256 83.196 us/op 82.744 us/op 1.01
bytes32 toHexString 300.00 ns/op 282.00 ns/op 1.06
bytes32 Buffer.toString(hex) 176.00 ns/op 164.00 ns/op 1.07
bytes32 Buffer.toString(hex) from Uint8Array 243.00 ns/op 233.00 ns/op 1.04
bytes32 Buffer.toString(hex) + 0x 180.00 ns/op 167.00 ns/op 1.08
Return object 10000 times 0.21460 ns/op 0.21190 ns/op 1.01
Throw Error 10000 times 3.4362 us/op 3.2921 us/op 1.04
toHex 100.68 ns/op 96.926 ns/op 1.04
Buffer.from 91.949 ns/op 88.275 ns/op 1.04
shared Buffer 64.670 ns/op 61.849 ns/op 1.05
fastMsgIdFn sha256 / 200 bytes 1.5170 us/op 1.4690 us/op 1.03
fastMsgIdFn h32 xxhash / 200 bytes 163.00 ns/op 150.00 ns/op 1.09
fastMsgIdFn h64 xxhash / 200 bytes 215.00 ns/op 204.00 ns/op 1.05
fastMsgIdFn sha256 / 1000 bytes 4.8110 us/op 4.7310 us/op 1.02
fastMsgIdFn h32 xxhash / 1000 bytes 248.00 ns/op 241.00 ns/op 1.03
fastMsgIdFn h64 xxhash / 1000 bytes 261.00 ns/op 254.00 ns/op 1.03
fastMsgIdFn sha256 / 10000 bytes 42.272 us/op 41.757 us/op 1.01
fastMsgIdFn h32 xxhash / 10000 bytes 1.2980 us/op 1.2830 us/op 1.01
fastMsgIdFn h64 xxhash / 10000 bytes 846.00 ns/op 836.00 ns/op 1.01
send data - 1000 256B messages 4.1010 ms/op 4.2612 ms/op 0.96
send data - 1000 512B messages 5.4032 ms/op 5.0634 ms/op 1.07
send data - 1000 1024B messages 6.1439 ms/op 5.3563 ms/op 1.15
send data - 1000 1200B messages 6.9227 ms/op 5.9433 ms/op 1.16
send data - 1000 2048B messages 9.2336 ms/op 9.0019 ms/op 1.03
send data - 1000 4096B messages 32.860 ms/op 87.008 ms/op 0.38
send data - 1000 16384B messages 233.81 ms/op 284.83 ms/op 0.82
send data - 1000 65536B messages 1.7004 s/op 1.2427 s/op 1.37
enrSubnets - fastDeserialize 64 bits 738.00 ns/op 792.00 ns/op 0.93
enrSubnets - ssz BitVector 64 bits 268.00 ns/op 293.00 ns/op 0.91
enrSubnets - fastDeserialize 4 bits 103.00 ns/op 102.00 ns/op 1.01
enrSubnets - ssz BitVector 4 bits 266.00 ns/op 290.00 ns/op 0.92
prioritizePeers score -10:0 att 32-0.1 sync 2-0 198.99 us/op 202.51 us/op 0.98
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 231.16 us/op 230.93 us/op 1.00
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 326.50 us/op 334.19 us/op 0.98
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 595.35 us/op 590.64 us/op 1.01
prioritizePeers score 0:0 att 64-1 sync 4-1 695.80 us/op 689.74 us/op 1.01
array of 16000 items push then shift 1.3093 us/op 1.2957 us/op 1.01
LinkedList of 16000 items push then shift 6.9340 ns/op 6.9240 ns/op 1.00
array of 16000 items push then pop 65.864 ns/op 67.525 ns/op 0.98
LinkedList of 16000 items push then pop 6.0070 ns/op 5.9920 ns/op 1.00
array of 24000 items push then shift 1.9253 us/op 1.9146 us/op 1.01
LinkedList of 24000 items push then shift 6.3630 ns/op 6.4270 ns/op 0.99
array of 24000 items push then pop 88.868 ns/op 101.19 ns/op 0.88
LinkedList of 24000 items push then pop 6.0080 ns/op 6.0000 ns/op 1.00
intersect bitArray bitLen 8 3.8980 ns/op 3.9850 ns/op 0.98
intersect array and set length 8 30.216 ns/op 30.314 ns/op 1.00
intersect bitArray bitLen 128 23.752 ns/op 23.385 ns/op 1.02
intersect array and set length 128 505.29 ns/op 501.52 ns/op 1.01
bitArray.getTrueBitIndexes() bitLen 128 929.00 ns/op 916.00 ns/op 1.01
bitArray.getTrueBitIndexes() bitLen 248 1.6660 us/op 1.6530 us/op 1.01
bitArray.getTrueBitIndexes() bitLen 512 3.4820 us/op 3.4370 us/op 1.01
Full columns - reconstruct all 6 blobs 177.85 us/op 204.74 us/op 0.87
Full columns - reconstruct half of the blobs out of 6 97.683 us/op 68.763 us/op 1.42
Full columns - reconstruct single blob out of 6 33.272 us/op 31.105 us/op 1.07
Half columns - reconstruct all 6 blobs 394.52 ms/op 395.96 ms/op 1.00
Half columns - reconstruct half of the blobs out of 6 198.22 ms/op 197.05 ms/op 1.01
Half columns - reconstruct single blob out of 6 70.380 ms/op 66.549 ms/op 1.06
Set add up to 64 items then delete first 1.5782 us/op 1.6549 us/op 0.95
OrderedSet add up to 64 items then delete first 2.4294 us/op 2.5030 us/op 0.97
Set add up to 64 items then delete last 1.7812 us/op 1.8736 us/op 0.95
OrderedSet add up to 64 items then delete last 2.7853 us/op 2.7538 us/op 1.01
Set add up to 64 items then delete middle 1.7882 us/op 1.8727 us/op 0.95
OrderedSet add up to 64 items then delete middle 4.2998 us/op 4.2096 us/op 1.02
Set add up to 128 items then delete first 3.5038 us/op 3.7071 us/op 0.95
OrderedSet add up to 128 items then delete first 5.2387 us/op 5.6644 us/op 0.92
Set add up to 128 items then delete last 3.4581 us/op 3.5694 us/op 0.97
OrderedSet add up to 128 items then delete last 5.5357 us/op 5.3098 us/op 1.04
Set add up to 128 items then delete middle 3.4746 us/op 3.7325 us/op 0.93
OrderedSet add up to 128 items then delete middle 11.349 us/op 11.408 us/op 0.99
Set add up to 256 items then delete first 6.9600 us/op 7.2593 us/op 0.96
OrderedSet add up to 256 items then delete first 11.234 us/op 12.096 us/op 0.93
Set add up to 256 items then delete last 6.9598 us/op 6.9856 us/op 1.00
OrderedSet add up to 256 items then delete last 11.806 us/op 11.065 us/op 1.07
Set add up to 256 items then delete middle 6.9591 us/op 7.0234 us/op 0.99
OrderedSet add up to 256 items then delete middle 34.846 us/op 34.499 us/op 1.01
runFastConfirmationRules vc:100000 bc:96 eq:0 4.3663 ms/op 4.6283 ms/op 0.94
runFastConfirmationRules vc:600000 bc:96 eq:0 38.477 ms/op 35.516 ms/op 1.08
runFastConfirmationRules vc:1000000 bc:96 eq:0 56.779 ms/op 56.505 ms/op 1.00
runFastConfirmationRules vc:600000 bc:320 eq:0 35.189 ms/op 33.927 ms/op 1.04
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.0578 s/op 1.3122 s/op 0.81
pass gossip attestations to forkchoice per slot 2.5625 ms/op 2.5764 ms/op 0.99
forkChoice updateHead vc 100000 bc 64 eq 0 401.89 us/op 440.58 us/op 0.91
forkChoice updateHead vc 600000 bc 64 eq 0 2.3839 ms/op 2.4923 ms/op 0.96
forkChoice updateHead vc 1000000 bc 64 eq 0 3.9527 ms/op 4.3349 ms/op 0.91
forkChoice updateHead vc 600000 bc 320 eq 0 2.3827 ms/op 2.6410 ms/op 0.90
forkChoice updateHead vc 600000 bc 1200 eq 0 2.4342 ms/op 2.6848 ms/op 0.91
forkChoice updateHead vc 600000 bc 7200 eq 0 2.7477 ms/op 3.0425 ms/op 0.90
forkChoice updateHead vc 600000 bc 64 eq 1000 2.3873 ms/op 2.6816 ms/op 0.89
forkChoice updateHead vc 600000 bc 64 eq 10000 2.5220 ms/op 2.7551 ms/op 0.92
forkChoice updateHead vc 600000 bc 64 eq 300000 6.3717 ms/op 6.7169 ms/op 0.95
computeDeltas 1400000 validators 0% inactive 11.757 ms/op 13.101 ms/op 0.90
computeDeltas 1400000 validators 10% inactive 11.345 ms/op 12.623 ms/op 0.90
computeDeltas 1400000 validators 20% inactive 10.595 ms/op 11.813 ms/op 0.90
computeDeltas 1400000 validators 50% inactive 8.6113 ms/op 9.5150 ms/op 0.91
computeDeltas 2100000 validators 0% inactive 17.726 ms/op 19.618 ms/op 0.90
computeDeltas 2100000 validators 10% inactive 16.796 ms/op 18.990 ms/op 0.88
computeDeltas 2100000 validators 20% inactive 15.687 ms/op 18.049 ms/op 0.87
computeDeltas 2100000 validators 50% inactive 10.360 ms/op 14.344 ms/op 0.72
altair processAttestation - 250000 vs - 7PWei normalcase 1.8091 ms/op 1.6681 ms/op 1.08
altair processAttestation - 250000 vs - 7PWei worstcase 2.5536 ms/op 2.4355 ms/op 1.05
altair processAttestation - setStatus - 1/6 committees join 102.14 us/op 98.990 us/op 1.03
altair processAttestation - setStatus - 1/3 committees join 195.56 us/op 194.79 us/op 1.00
altair processAttestation - setStatus - 1/2 committees join 272.34 us/op 274.50 us/op 0.99
altair processAttestation - setStatus - 2/3 committees join 359.30 us/op 373.84 us/op 0.96
altair processAttestation - setStatus - 4/5 committees join 487.14 us/op 512.13 us/op 0.95
altair processAttestation - setStatus - 100% committees join 582.13 us/op 597.91 us/op 0.97
altair processBlock - 250000 vs - 7PWei normalcase 3.7651 ms/op 3.1148 ms/op 1.21
altair processBlock - 250000 vs - 7PWei normalcase hashState 13.429 ms/op 13.877 ms/op 0.97
altair processBlock - 250000 vs - 7PWei worstcase 21.518 ms/op 21.922 ms/op 0.98
altair processBlock - 250000 vs - 7PWei worstcase hashState 43.200 ms/op 40.926 ms/op 1.06
phase0 processBlock - 250000 vs - 7PWei normalcase 1.4011 ms/op 1.3009 ms/op 1.08
phase0 processBlock - 250000 vs - 7PWei worstcase 16.496 ms/op 16.021 ms/op 1.03
altair processEth1Data - 250000 vs - 7PWei normalcase 282.86 us/op 294.95 us/op 0.96
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.3550 us/op 3.2200 us/op 1.04
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 21.410 us/op 22.594 us/op 0.95
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.0730 us/op 5.9940 us/op 1.01
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.2590 us/op 3.9640 us/op 0.82
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 82.926 us/op 96.247 us/op 0.86
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.3506 ms/op 1.3919 ms/op 0.97
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.7129 ms/op 1.8527 ms/op 0.92
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.7026 ms/op 1.8429 ms/op 0.92
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.4858 ms/op 3.6709 ms/op 0.95
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.9437 ms/op 2.1789 ms/op 0.89
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 3.9517 ms/op 3.9853 ms/op 0.99
Tree 40 250000 create 344.48 ms/op 339.59 ms/op 1.01
Tree 40 250000 get(125000) 104.17 ns/op 100.02 ns/op 1.04
Tree 40 250000 set(125000) 1.1040 us/op 1.0243 us/op 1.08
Tree 40 250000 toArray() 9.3034 ms/op 18.460 ms/op 0.50
Tree 40 250000 iterate all - toArray() + loop 9.4571 ms/op 16.536 ms/op 0.57
Tree 40 250000 iterate all - get(i) 35.170 ms/op 38.086 ms/op 0.92
Array 250000 create 2.0385 ms/op 2.2881 ms/op 0.89
Array 250000 clone - spread 645.33 us/op 691.31 us/op 0.93
Array 250000 get(125000) 0.30200 ns/op 0.29400 ns/op 1.03
Array 250000 set(125000) 0.30300 ns/op 0.29400 ns/op 1.03
Array 250000 iterate all - loop 57.173 us/op 56.902 us/op 1.00
phase0 afterProcessEpoch - 250000 vs - 7PWei 38.946 ms/op 48.353 ms/op 0.81
Array.fill - length 1000000 1.9214 ms/op 2.2771 ms/op 0.84
Array push - length 1000000 7.7004 ms/op 7.7597 ms/op 0.99
Array.get 0.20540 ns/op 0.20352 ns/op 1.01
Uint8Array.get 0.25967 ns/op 0.24689 ns/op 1.05
phase0 beforeProcessEpoch - 250000 vs - 7PWei 17.224 ms/op 11.621 ms/op 1.48
altair processEpoch - mainnet_e81889 298.55 ms/op 240.57 ms/op 1.24
mainnet_e81889 - altair beforeProcessEpoch 16.965 ms/op 18.834 ms/op 0.90
mainnet_e81889 - altair processJustificationAndFinalization 8.6220 us/op 5.0840 us/op 1.70
mainnet_e81889 - altair processInactivityUpdates 5.2112 ms/op 3.7210 ms/op 1.40
mainnet_e81889 - altair processRewardsAndPenalties 19.622 ms/op 17.087 ms/op 1.15
mainnet_e81889 - altair processRegistryUpdates 559.00 ns/op 556.00 ns/op 1.01
mainnet_e81889 - altair processSlashings 147.00 ns/op 137.00 ns/op 1.07
mainnet_e81889 - altair processEth1DataReset 143.00 ns/op 137.00 ns/op 1.04
mainnet_e81889 - altair processEffectiveBalanceUpdates 2.1308 ms/op 1.1684 ms/op 1.82
mainnet_e81889 - altair processSlashingsReset 702.00 ns/op 693.00 ns/op 1.01
mainnet_e81889 - altair processRandaoMixesReset 1.3710 us/op 1.3420 us/op 1.02
mainnet_e81889 - altair processHistoricalRootsUpdate 141.00 ns/op 141.00 ns/op 1.00
mainnet_e81889 - altair processParticipationFlagUpdates 439.00 ns/op 447.00 ns/op 0.98
mainnet_e81889 - altair processSyncCommitteeUpdates 120.00 ns/op 118.00 ns/op 1.02
mainnet_e81889 - altair afterProcessEpoch 41.721 ms/op 42.228 ms/op 0.99
capella processEpoch - mainnet_e217614 1.0391 s/op 716.08 ms/op 1.45
mainnet_e217614 - capella beforeProcessEpoch 62.807 ms/op 56.941 ms/op 1.10
mainnet_e217614 - capella processJustificationAndFinalization 7.2790 us/op 5.1850 us/op 1.40
mainnet_e217614 - capella processInactivityUpdates 14.505 ms/op 11.762 ms/op 1.23
mainnet_e217614 - capella processRewardsAndPenalties 96.603 ms/op 93.083 ms/op 1.04
mainnet_e217614 - capella processRegistryUpdates 4.6110 us/op 4.5220 us/op 1.02
mainnet_e217614 - capella processSlashings 145.00 ns/op 141.00 ns/op 1.03
mainnet_e217614 - capella processEth1DataReset 141.00 ns/op 135.00 ns/op 1.04
mainnet_e217614 - capella processEffectiveBalanceUpdates 4.3332 ms/op 5.2193 ms/op 0.83
mainnet_e217614 - capella processSlashingsReset 714.00 ns/op 677.00 ns/op 1.05
mainnet_e217614 - capella processRandaoMixesReset 1.4050 us/op 1.0840 us/op 1.30
mainnet_e217614 - capella processHistoricalRootsUpdate 140.00 ns/op 136.00 ns/op 1.03
mainnet_e217614 - capella processParticipationFlagUpdates 451.00 ns/op 445.00 ns/op 1.01
mainnet_e217614 - capella afterProcessEpoch 112.37 ms/op 109.14 ms/op 1.03
phase0 processEpoch - mainnet_e58758 321.99 ms/op 251.76 ms/op 1.28
mainnet_e58758 - phase0 beforeProcessEpoch 67.149 ms/op 45.365 ms/op 1.48
mainnet_e58758 - phase0 processJustificationAndFinalization 6.6700 us/op 4.7920 us/op 1.39
mainnet_e58758 - phase0 processRewardsAndPenalties 17.252 ms/op 15.323 ms/op 1.13
mainnet_e58758 - phase0 processRegistryUpdates 2.2820 us/op 2.2450 us/op 1.02
mainnet_e58758 - phase0 processSlashings 146.00 ns/op 136.00 ns/op 1.07
mainnet_e58758 - phase0 processEth1DataReset 137.00 ns/op 135.00 ns/op 1.01
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 819.51 us/op 1.0109 ms/op 0.81
mainnet_e58758 - phase0 processSlashingsReset 894.00 ns/op 807.00 ns/op 1.11
mainnet_e58758 - phase0 processRandaoMixesReset 1.8940 us/op 1.0070 us/op 1.88
mainnet_e58758 - phase0 processHistoricalRootsUpdate 145.00 ns/op 136.00 ns/op 1.07
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.2350 us/op 974.00 ns/op 1.27
mainnet_e58758 - phase0 afterProcessEpoch 33.133 ms/op 32.956 ms/op 1.01
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.0220 ms/op 1.0056 ms/op 1.02
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.6416 ms/op 1.6175 ms/op 1.01
altair processInactivityUpdates - 250000 normalcase 10.585 ms/op 10.363 ms/op 1.02
altair processInactivityUpdates - 250000 worstcase 10.696 ms/op 10.327 ms/op 1.04
phase0 processRegistryUpdates - 250000 normalcase 2.2890 us/op 2.3800 us/op 0.96
phase0 processRegistryUpdates - 250000 badcase_full_deposits 144.74 us/op 146.84 us/op 0.99
phase0 processRegistryUpdates - 250000 worstcase 0.5 65.319 ms/op 58.651 ms/op 1.11
altair processRewardsAndPenalties - 250000 normalcase 13.994 ms/op 14.345 ms/op 0.98
altair processRewardsAndPenalties - 250000 worstcase 13.686 ms/op 14.060 ms/op 0.97
phase0 getAttestationDeltas - 250000 normalcase 5.8803 ms/op 5.4425 ms/op 1.08
phase0 getAttestationDeltas - 250000 worstcase 7.6204 ms/op 5.4401 ms/op 1.40
phase0 processSlashings - 250000 worstcase 58.989 us/op 62.018 us/op 0.95
altair processSyncCommitteeUpdates - 250000 10.115 ms/op 9.9483 ms/op 1.02
BeaconState.hashTreeRoot - No change 182.00 ns/op 168.00 ns/op 1.08
BeaconState.hashTreeRoot - 1 full validator 63.625 us/op 59.262 us/op 1.07
BeaconState.hashTreeRoot - 32 full validator 1.0430 ms/op 668.50 us/op 1.56
BeaconState.hashTreeRoot - 512 full validator 7.4906 ms/op 6.4278 ms/op 1.17
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 109.05 us/op 73.867 us/op 1.48
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.5312 ms/op 1.0805 ms/op 1.42
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 24.019 ms/op 13.309 ms/op 1.80
BeaconState.hashTreeRoot - 1 balances 84.886 us/op 59.609 us/op 1.42
BeaconState.hashTreeRoot - 32 balances 783.44 us/op 572.60 us/op 1.37
BeaconState.hashTreeRoot - 512 balances 5.3951 ms/op 4.9639 ms/op 1.09
BeaconState.hashTreeRoot - 250000 balances 166.11 ms/op 115.59 ms/op 1.44
aggregationBits - 2048 els - zipIndexesInBitList 20.296 us/op 19.925 us/op 1.02
regular array get 100000 times 22.753 us/op 22.919 us/op 0.99
wrappedArray get 100000 times 22.752 us/op 22.812 us/op 1.00
arrayWithProxy get 100000 times 16.557 ms/op 10.801 ms/op 1.53
ssz.Root.equals 21.593 ns/op 21.462 ns/op 1.01
byteArrayEquals 21.335 ns/op 21.268 ns/op 1.00
Buffer.compare 9.0560 ns/op 8.8890 ns/op 1.02
processSlot - 1 slots 11.616 us/op 7.9580 us/op 1.46
processSlot - 32 slots 2.4850 ms/op 1.5512 ms/op 1.60
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.8122 ms/op 2.7305 ms/op 1.76
getCommitteeAssignments - req 1 vs - 250000 vc 1.6674 ms/op 1.6610 ms/op 1.00
getCommitteeAssignments - req 100 vs - 250000 vc 3.4222 ms/op 3.3854 ms/op 1.01
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6622 ms/op 3.6297 ms/op 1.01
findModifiedValidators - 10000 modified validators 675.36 ms/op 755.06 ms/op 0.89
findModifiedValidators - 1000 modified validators 472.60 ms/op 534.45 ms/op 0.88
findModifiedValidators - 100 modified validators 220.38 ms/op 273.28 ms/op 0.81
findModifiedValidators - 10 modified validators 210.18 ms/op 249.96 ms/op 0.84
findModifiedValidators - 1 modified validators 151.84 ms/op 172.70 ms/op 0.88
findModifiedValidators - no difference 143.01 ms/op 146.58 ms/op 0.98
migrate state 1500000 validators, 3400 modified, 2000 new 3.3485 s/op 2.6970 s/op 1.24
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7200 ns/op 3.7400 ns/op 0.99
state getBlockRootAtSlot - 250000 vs - 7PWei 451.07 ns/op 281.27 ns/op 1.60
computeProposerIndex 100000 validators 1.3024 ms/op 1.3382 ms/op 0.97
getNextSyncCommitteeIndices 1000 validators 2.8575 ms/op 2.8411 ms/op 1.01
getNextSyncCommitteeIndices 10000 validators 24.892 ms/op 25.280 ms/op 0.98
getNextSyncCommitteeIndices 100000 validators 83.239 ms/op 84.642 ms/op 0.98
computeProposers - vc 250000 546.13 us/op 552.15 us/op 0.99
computeEpochShuffling - vc 250000 39.020 ms/op 40.934 ms/op 0.95
getNextSyncCommittee - vc 250000 9.7982 ms/op 9.4627 ms/op 1.04
nodejs block root to RootHex using toHex 96.934 ns/op 99.706 ns/op 0.97
nodejs block root to RootHex using toRootHex 61.424 ns/op 65.398 ns/op 0.94
nodejs fromHex(blob) 895.65 us/op 729.74 us/op 1.23
nodejs fromHexInto(blob) 632.71 us/op 655.74 us/op 0.96
nodejs block root to RootHex using the deprecated toHexString 455.03 ns/op 486.80 ns/op 0.93
nodejs byteArrayEquals 32 bytes (block root) 25.842 ns/op 25.861 ns/op 1.00
nodejs byteArrayEquals 48 bytes (pubkey) 37.594 ns/op 37.351 ns/op 1.01
nodejs byteArrayEquals 96 bytes (signature) 34.115 ns/op 39.596 ns/op 0.86
nodejs byteArrayEquals 1024 bytes 40.275 ns/op 43.597 ns/op 0.92
nodejs byteArrayEquals 131072 bytes (blob) 1.7713 us/op 1.7686 us/op 1.00
browser block root to RootHex using toHex 144.02 ns/op 145.00 ns/op 0.99
browser block root to RootHex using toRootHex 130.40 ns/op 130.83 ns/op 1.00
browser fromHex(blob) 1.6725 ms/op 1.5697 ms/op 1.07
browser fromHexInto(blob) 633.68 us/op 661.21 us/op 0.96
browser block root to RootHex using the deprecated toHexString 310.89 ns/op 347.84 ns/op 0.89
browser byteArrayEquals 32 bytes (block root) 27.247 ns/op 27.653 ns/op 0.99
browser byteArrayEquals 48 bytes (pubkey) 38.495 ns/op 39.042 ns/op 0.99
browser byteArrayEquals 96 bytes (signature) 71.957 ns/op 72.868 ns/op 0.99
browser byteArrayEquals 1024 bytes 733.79 ns/op 745.96 ns/op 0.98
browser byteArrayEquals 131072 bytes (blob) 92.269 us/op 93.823 us/op 0.98

by benchmarkbot/action

Addresses review feedback. If a worker misses the retry deadline but terminates
while the rest of `BeaconNode.close()` runs, it can no longer block
`process.exit()`, so forcing the exit would turn a clean shutdown into 143 for
no reason. Count unterminated workers rather than latching a flag, so one
terminating late does not clear the record for another that is genuinely stuck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nflaig

nflaig commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Closing this, the approach does not generalise.

It only works when the node is not pid 1. The kernel discards signals sent to pid 1 that have no handler installed, including SIGKILL, so a container running the node directly as pid 1 cannot signal itself at all:

result
docker run node -e 'process.kill(process.pid,"SIGKILL")' pid=1, survives, exit 0
same with docker run --init pid=7, killed immediately

That covers systemd and init-enabled containers but silently does nothing for default docker and Kubernetes, which is most deployments. Making it work needs init: true or tini per deployment, and there are plenty of custom setups that would not get it. Behaviour that varies by environment is worse than one consistent behaviour, and force exiting is the process manager's job anyway, it does it correctly from outside the pid namespace where signals actually work.

Worth recording for whoever looks at this next: this also explains the earlier attempt in #9790 that appeared to fail. The flag propagation was likely fine, the kill simply did nothing, and I misattributed it. Synchronous fs.writeSync(2, ...) probes from a real shutdown:

DBG reached-check flag=true
DBG inside-if
DBG listeners-after-remove=0
DBG survived-SIGTERM
DBG survived-SIGKILL (impossible)

process.abort() does terminate as pid 1, glibc falls back rather than relying on delivery, but it exits 139 with a native stack dump and would attempt a core dump of the whole heap, ~7GB on the node measured. Not a good trade for avoiding a 60s wait.

The real fix remains the root cause, a libuv handle in the network worker that never closes so the thread spins in Environment::CleanupHandles(). Notes at https://gist.github.com/nflaig/5f41cfc50f38baf5046a034162943dc3

@nflaig nflaig closed this Aug 9, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Lodestar Team Coordination Aug 9, 2026
@nflaig

nflaig commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

I told my claude to close this, seems a bit too hacky to me. but giving up on debugging the root cause now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant